package service

import (
	"MySQL_BackUp/models"
	"database/sql"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/shirou/gopsutil/disk"
)

// MySQLBackupService is a struct that implements BackupService
type MySQLBackupService struct {
	Config *models.Config
	Db     *sql.DB
}

// 检查路径是否存在,如果不存在则创建
func (b *MySQLBackupService) CheckAndCreateDirectories(directories []string) error {
	for _, dir := range directories {
		//获取目录信息,并判断目录是否存在,如果不存在则创建
		if _, err := os.Stat(dir); os.IsNotExist(err) {
			err := os.MkdirAll(dir, os.ModePerm)
			if err != nil {
				return fmt.Errorf("failed to create directory %s: %v", dir, err)
			}
			log.Printf("Directory %s created\n", dir)
		} else {
			log.Printf("Directory %s already exists\n", dir)
		}
	}
	return nil
}

// 检查当前是否存在备份进程
func (b *MySQLBackupService) CheckXtarPid() bool {
	//golang执行shell命令标准库 exec.Command("xx","xx")创建新的*cmd对象
	cmd := exec.Command("pgrep", "-f", "xtra")
	// 使用Output方法执行该命令并收集其输出
	output, err := cmd.Output()
	if err == nil && len(output) > 0 {
		log.Printf("# 当前存在备份进程：\n%s", output)
		return true
	} else {
		return false
	}
}
func (b *MySQLBackupService) CheckDevUsage() {
	//使用第三方disk库文件获取磁盘使用率
	usageStat, err := disk.Usage(b.Config.BackupPath)
	if err != nil {
		log.Fatalf("Failed to get disk usage: %v", err)
	}
	devUsage := usageStat.UsedPercent

	//判断磁盘使用率是否超过了80%
	if devUsage >= 80 {
		log.Printf("# disk dev_usage more than 80%%: %.2f\n", devUsage)
		//遍历执行目录下的所有文件 filepath.Walk()函数本身具有递归调用功能
		err := filepath.Walk(b.Config.BackupPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			//判断指定目录下的文件及文件日期
			if !info.IsDir() && time.Since(info.ModTime()).Hours() > 24*7 {
				if err := os.Remove(path); err != nil {
					return err
				}
			}

			return nil
		})

		if err != nil {
			log.Fatalf("# failed to delete old backup files: %s\n", err.Error())
		}
		log.Printf("# delete backup files older than 7 days\n")
	}
}

func (b *MySQLBackupService) BackupDatabase() {
	//判断备份文件是否存在，如果不存在则创建
	log.Printf("# 开始全备份...")
	backupFile := fmt.Sprintf("%s/full_%s.tar.gz", b.Config.BackupPath, time.Now().Format("20060102"))
	file, err := os.Create(backupFile)
	if err != nil {
		log.Fatalf("# failed to create backup file: %s\n", err.Error())
	}
	defer file.Close()

	/*
		1、拼接日志文件路径
		2、使用os.OpenFile()函数打开日志文件,如果不存在则创建
	*/
	logFilePath := filepath.Join(b.Config.LogPath, b.Config.LogFile)
	logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatalf("# failed to open log file: %s\n", err.Error())
	}
	defer logFile.Close()
	/*
		执行xtrbackup备份命令，并启用多线程
		 $xtrabackup --defaults-file=${mycnf} --user=$username --password=$passwd --slave-info   --backup  --lock-ddl-per-table
		 	--stream=tar  2>> $log_file | gzip - > ${backup_path}/full_${current_day}.tar.gz
	*/
	cmd := exec.Command(b.Config.Xtrabackup,
		"--defaults-file="+b.Config.MyCnf,
		"--user="+b.Config.Username,
		"--password="+b.Config.Password,
		"--slave-info",
		"--backup",
		"--lock-ddl-per-table",
		"--stream=tar",
		"--parallel="+b.Config.Parallel,
	)

	//执行gzip压缩命令
	gzipCmd := exec.Command("gzip")

	//因为原备份命令是多命令组合而成的，因此使用管道 cmd.StdoutPipe()
	stdoutPipe, err := cmd.StdoutPipe()
	if err != nil {
		log.Fatalf("# failed to get stdout pipe: %s\n", err.Error())
	}
	defer stdoutPipe.Close()

	//执行命令,并区分stdin、stdout 和 stderr
	/*
		这行代码将gzipCmd命令的标准输入（stdin）设置为另一个命令的标准输出管道（stdoutPipe）。
		stdoutPipe是一个io.Reader，通常由前一个命令创建，用于从其标准输出读取数据。
		这样做的目的是将前一个命令的输出直接作为gzipCmd的输入，实现数据流的连接。
	*/
	gzipCmd.Stdin = stdoutPipe
	gzipCmd.Stdout = file    //标准输出 到备份文件backupfile中
	cmd.Stderr = logFile     //备份命令标准错误  输出到日志文件full_backup_data_{{ .current_day }}.log
	gzipCmd.Stderr = logFile //gzip压缩命令标准错误  输出到日志文件full_backup_data_{{ .current_day }}.log

	/*
			cmd.Run()和cmd.Start()的区别:
				cmd.Run() 执行 Run 会立即阻塞当前goroutine 等待 5 秒种
				Run() 方法适用于需要顺序执行外部命令

		 		cmd.Start() 不用等命令执行完成，就结束, 然后在 Wait() 方法阻塞等待 5s
				使用 Start() 方法适用于需要并发执行多个命令的情况，
				wait()方法必须与start结合使用
	*/
	log.Printf("# 开始执行 gzip 命令...")
	if err := gzipCmd.Start(); err != nil {
		log.Fatalf("# failed to start gzip command: %s\n", err.Error())
	}

	log.Printf("# 开始执行 xtrabackup 命令...")
	if err := cmd.Start(); err != nil {
		log.Fatalf("# failed to start xtrabackup command: %s\n", err.Error())
	}

	if err := cmd.Wait(); err != nil {
		log.Fatalf("# xtrabackup command failed: %s\n", err.Error())
	}

	if err := gzipCmd.Wait(); err != nil {
		log.Fatalf("# gzip command failed: %s\n", err.Error())
	}
	log.Printf("# 确认备份文件是否成功创建及压缩成功...")
	// 检查备份文件是否存在
	if _, err := os.Stat(backupFile); err != nil {
		log.Fatalf("# failed to stat backup file: %s\n", err.Error())
	} else {
		log.Printf("# 备份文件成功创建并压缩成功\n")
	}
	log.Printf("# 全备完毕...\n")
}

// 备份binlog
func (b *MySQLBackupService) BackupBinlog() {
	log.Printf("# 开始增量备份binlog...")
	binlogFilePath := filepath.Join(b.Config.BinlogPath, b.Config.BinlogFile)
	binlogFile, err := os.OpenFile(binlogFilePath, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatalf("# failed to open binlog file: %s\n", err.Error())
	}
	defer binlogFile.Close()

	// 函数写入日志
	writeLog := func(message string) {
		log.Println(message)
		binlogFile.WriteString(message + "\n")
	}

	// 查找所有匹配的binlog文件，并按升序排序
	cmd1 := exec.Command("sh", "-c", fmt.Sprintf("for i in $(find %s -name 'mysql-bin.*' | grep -v mysql-bin.index | sort -n | sed '$d'); do if [ ! -e %s/$(basename $i) ]; then cp -p $i %s; cp -p $i %s; fi; done", b.Config.BinlogPathFileSrc, b.Config.BinlogPath, b.Config.BinlogPath, b.Config.BinlogPath))
	output1, err := cmd1.CombinedOutput()
	if err != nil {
		writeLog(fmt.Sprintf("# %s 备份binlog失败: %s\n%s", time.Now().Format("2006-01-02 15:04:05"), err.Error(), output1))
	} else {
		writeLog(fmt.Sprintf("# %s 备份binlog成功:\n%s", time.Now().Format("2006-01-02 15:04:05"), output1))
	}

	// 复制除了最后一个文件之外的所有文件到备份目录（如果它们还不存在）。 复制最新的binlog文件到备份目录。
	cmd2 := exec.Command("sh", "-c", fmt.Sprintf("for i in $(find %s -name 'mysql-bin.*' | grep -v mysql-bin.index | sort -nr | head -1); do cp -p $i %s; cp -p $i %s; done", b.Config.BinlogPathFileSrc, b.Config.BinlogPath, b.Config.BinlogPath))
	output2, err := cmd2.CombinedOutput()
	if err != nil {
		writeLog(fmt.Sprintf("# %s 复制最新的binlog文件到备份目录失败: %s\n%s", time.Now().Format("2006-01-02 15:04:05"), err.Error(), output2))
	} else {
		writeLog(fmt.Sprintf("# %s 复制最新的binlog文件到备份目录成功:\n%s", time.Now().Format("2006-01-02 15:04:05"), output2))
	}

	// 删除过期备份binlog数据，只保留一个备份周期7天的数据
	deleteCmd := exec.Command("find", b.Config.BinlogPath, "-name", "mysql-bin.*", "-ctime", "+6", "-exec", "rm", "-f", "{}", ";")
	output3, err := deleteCmd.CombinedOutput()
	if err != nil {
		writeLog(fmt.Sprintf("# %s 删除过期备份binlog数据失败: %s\n%s", time.Now().Format("2006-01-02 15:04:05"), err.Error(), output3))
	} else {
		writeLog(fmt.Sprintf("# %s 删除过期备份binlog数据成功:\n%s", time.Now().Format("2006-01-02 15:04:05"), output3))
	}

	writeLog("# 增量备份完毕...")
}
